package semant.second_pass;

import semant.Env;
import symbol.ClassInfo;
import symbol.MethodInfo;
import symbol.Symbol;
import symbol.VarInfo;
import syntaxtree.And;
import syntaxtree.ArrayLength;
import syntaxtree.ArrayLookup;
import syntaxtree.BooleanType;
import syntaxtree.Call;
import syntaxtree.Equal;
import syntaxtree.Exp;
import syntaxtree.False;
import syntaxtree.IdentifierExp;
import syntaxtree.IdentifierType;
import syntaxtree.IntArrayType;
import syntaxtree.IntegerLiteral;
import syntaxtree.IntegerType;
import syntaxtree.LessThan;
import syntaxtree.Minus;
import syntaxtree.NewArray;
import syntaxtree.NewObject;
import syntaxtree.Not;
import syntaxtree.Plus;
import syntaxtree.This;
import syntaxtree.Times;
import syntaxtree.True;
import syntaxtree.Type;
import syntaxtree.VisitorAdapter;
import util.List;

class ExpHandler extends VisitorAdapter{
    private Env env;
    private ClassInfo cinfo;
    private MethodInfo minfo;
    private Type type;

    private ExpHandler(Env e, ClassInfo c, MethodInfo m)
    {
        env = e;
        cinfo = c;
        minfo = m;
        type = null;
    }

    static Type secondPass(Env env, ClassInfo cinfo, MethodInfo minfo, Exp node) {
        ExpHandler h = new ExpHandler(env, cinfo, minfo);
        node.accept(h);
        node.type = h.type;
        return h.type;
    }

    @Override
    public void visit(LessThan node) {
    	Type lhs = ExpHandler.secondPass(env, cinfo, minfo, node.lhs);
        Type rhs = ExpHandler.secondPass(env, cinfo, minfo, node.rhs);
        
        if (!(lhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado esquerdo do operador '<': " + lhs
				}
				);
		}
        
        if (!(rhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado direito do operador '<': " + rhs
				}
				);
		}

        // armazenamos o tipo de retorno da expressao
        this.type = new BooleanType(node.line, node.row); 
    }

    @Override
    public void visit(Equal node) {
    	Type lhs = ExpHandler.secondPass(env, cinfo, minfo, node.lhs);
        Type rhs = ExpHandler.secondPass(env, cinfo, minfo, node.rhs);
        
        if (!(lhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado esquerdo do operador '==': " + lhs
				}
				);
		}
        
        if (!(rhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado direito do operador '==': " + rhs
				}
				);
		}

        // armazenamos o tipo de retorno da expressao
        this.type = new BooleanType(node.line, node.row); 
    }

    @Override
    public void visit(Plus node) {
    	Type lhs = ExpHandler.secondPass(env, cinfo, minfo, node.lhs);
        Type rhs = ExpHandler.secondPass(env, cinfo, minfo, node.rhs);
        
        if (!(lhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado esquerdo do operador '+': " + lhs
				}
				);
		}
        
        if (!(rhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado direito do operador '+': " + rhs
				}
				);
		}
        
        // armazenamos o tipo de retorno da expressao
        this.type = new IntegerType(node.line, node.row); 
    }

    @Override
    public void visit(Minus node) {
    	Type lhs = ExpHandler.secondPass(env, cinfo, minfo, node.lhs);
        Type rhs = ExpHandler.secondPass(env, cinfo, minfo, node.rhs);
        
        if (!(lhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado esquerdo do operador '-': " + lhs
				}
				);
		}
        
        if (!(rhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado direito do operador '-': " + rhs
				}
				);
		}
        
        // armazenamos o tipo de retorno da expressao
        this.type = new IntegerType(node.line, node.row);
    }

    @Override
    public void visit(Times node) {
    	Type lhs = ExpHandler.secondPass(env, cinfo, minfo, node.lhs);
        Type rhs = ExpHandler.secondPass(env, cinfo, minfo, node.rhs);
        
        if (!(lhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado esquerdo do operador '*': " + lhs
				}
				);
		}
        
        if (!(rhs instanceof IntegerType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao inteiro encontrado no lado direito do operador '*': " + rhs
				}
				);
		}

        // armazenamos o tipo de retorno da expressao
        this.type = new IntegerType(node.line, node.row);
    }

    @Override
    public void visit(ArrayLookup node) {
    	// array[index]
    	// array precisa ser do tipo array
    	// index precisa ser do tipo inteiro
    	
    	Type array = ExpHandler.secondPass(env, cinfo, minfo, node.array);
    	Type index = ExpHandler.secondPass(env, cinfo, minfo, node.index);
    	
    	if (!(array instanceof IntArrayType)) {
			env.err.Error(
				node.array, 
				new Object[] {
					"A expressao precisa ser um array, mas foi resolvida como: " + array
				}
				);
		}
    	
    	if (!(index instanceof IntegerType)) {
			env.err.Error(
				node.array, 
				new Object[] {
					"Indice do operador [] precisa ser inteiro. Encontrado: " + index
				}
				);
		}
    	

        // armazenamos o tipo de retorno da expressao
    	// no caso, os arrays soh armazenam ints
        this.type = new IntegerType(node.line, node.row);
    }

    @Override
    public void visit(ArrayLength node) {
    	// array.lenght
    	// array precisa ser do tipo array
    	
    	Type array = ExpHandler.secondPass(env, cinfo, minfo, node.array);
    	
    	if (!(array instanceof IntArrayType)) {
			env.err.Error(
				node.array, 
				new Object[] {
					"A expressao precisa ser um array, mas foi resolvida como: " + array
				}
				);
		}
    	
        // armazenamos o tipo de retorno da expressao
        this.type = new IntegerType(node.line, node.row);
    }

    @Override
    public void visit(Call node) {
    	// x.m(a, b, c)
    	// m precisa estar declarado na classe de x
    	// numero e tipos dos parametros precisam
    	// ser iguais aos de m
        
    	Type class_t = ExpHandler.secondPass(env, cinfo, minfo, node.object);
    	
    	// verifica se x eh uma instancia de uma classe
    	if (!(class_t instanceof IdentifierType)) {
    		env.err.Error(
    				node, 
    				new Object[] {
    					"O tipo " + class_t + "nai suporta chamadas de metodos."
    				}
    				);
    		
    		// por padrao a gente retorna um int
    		this.type = new IntegerType(node.line, node.row);
		}
    	
    	ClassInfo ci = env.classes.get(Symbol.symbol(class_t.toString()));
    	MethodInfo mi = ExpHandler.getMethod(ci, node.method.s);
    	
    	if (mi == null) { // checa se o metodo foi declarado na classe de x
    		env.err.Error(
    				node, 
    				new Object[] {
    					"Metodo " + node.method.s + " nao definido na classe " + ci.name.toString()
    				}
    				);
    		
    		// por padrao a gente retorna um int
    		this.type = new IntegerType(node.line, node.row);
    		return;
		} 
    	
    	// agora, para cada parametro p de node.actuals
		// ele deve estar declarado e ser compativel com o formal
		// correspondente no mi.formals
    	List<VarInfo> f;
    	List<Exp> actuals = node.actuals;
		int i;
    	for (f = mi.formals, i = 1; f != null && actuals != null; actuals = actuals.tail, f = f.tail, i++) {
			// O teste de declara��o � feito na secondPass sobre a expression
			Type t = ExpHandler.secondPass(env, cinfo, minfo, actuals.head);
			if(!TypeHandler.compatible(env, f.head.type, t))
			{
				env.err.Error(
	    				node, 
	    				new Object[] {
	    					"Parametro #" + i + " incompativel no metodo " + node.method.s,
	    					"Esperado: " +  f.head.type,
	    					"Encontrado: " + t
	    				}
	    				);
			}
		}
    	
    	// se sobrou alguma coisa nas listas, temos um erro de numero de parametros
    	if (actuals != null || f != null) {
    		env.err.Error(
    				node, 
    				new Object[] {
    					"Numero incorreto de parametros do metodo " + node.method.s,
    				}
    				);
		}
    	
    	// finalmente, o tipo de retorno eh o do metodo.
    	this.type = mi.type;
    	
    }

    @Override
    public void visit(True node) {
        this.type = new BooleanType(node.line, node.row);
    }

    @Override
    public void visit(False node) {
    	this.type = new BooleanType(node.line, node.row);
    }

    @Override
    public void visit(This node) {
    	if (this.minfo.name.toString() == "Main") {
			this.env.err.Error(node, new Object[]{ "'this' nao pode ser usado no metodo Main" });
		}
    	
		this.type = new IdentifierType(node.line, node.row, cinfo.name.toString());
    }

    @Override
    public void visit(NewArray node) {
        // {new_array} [token]:int [size]:exp
    	// exp precisa retornar um inteiro
    	Type t = ExpHandler.secondPass(env, cinfo, minfo, node.size);
    	
    	if (!(t instanceof IntegerType)) {
			env.err.Error(
				node.size, 
				new Object[] {
					"Tamanho do array precisa ser inteiro. Encontrado: " + t
				}
				);
		}
    	
    	this.type = new IntArrayType(node.line, node.row);
    }

    @Override
    public void visit(NewObject node) {
    	// {new_object} [name]:id
    	// id precisa ser uma classe conhecida
    	Symbol cname = Symbol.symbol(node.className.s);
    	ClassInfo cl = env.classes.get(cname);
    	
    	if (cl == null) {
			this.env.err.Error(node, new Object[] {"Classe nao encontrada: " + node.className.s});
		}
    	
    	this.type = new IdentifierType(node.line, node.row, node.className.s);
    }

    @Override
    public void visit(Not node) {
        Type t = ExpHandler.secondPass(env, cinfo, minfo, node.exp);
        
        if (!(t instanceof BooleanType)) {
			env.err.Error(node.exp, 
					new Object[] {
						"Tipo incorreto para o operador '!'",
						"Esperado: Boolean",
						"Encontrado: " + t
						});
		}
        
        this.type = new BooleanType(node.line, node.row);
    }
    
    @Override
	public void visit(And node) {
    	Type lhs = ExpHandler.secondPass(env, cinfo, minfo, node.lhs);
        Type rhs = ExpHandler.secondPass(env, cinfo, minfo, node.rhs);
        
        if (!(lhs instanceof BooleanType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao boolean encontrado no lado esquerdo do operador '&&': " + lhs
				}
				);
		}
        
        if (!(rhs instanceof BooleanType)) {
			env.err.Error(
				node.lhs, 
				new Object[] {
					"Tipo nao boolean encontrado no lado direito do operador '&&': " + rhs
				}
				);
		}

        // armazenamos o tipo de retorno da expressao
        this.type = new BooleanType(node.line, node.row);
	}

	@Override
    public void visit(IdentifierExp node) {
		// {identifier} [name]:id
		// id precisa ser uma variavel declarada
		Symbol vname = Symbol.symbol(node.name.s);
		VarInfo var = ExpHandler.getVariable(cinfo, minfo, vname);
		
		if (var == null) {		
			// se a variavel nao existe, informamos um erro
            env.err.Error(node, new Object[]{"Variavel nao declarada: ", vname});
        } else { 
        	// caso exista, armazenamos o seu tipo
        	this.type = var.type;
        }
	}

	@Override
	public void visit(IntegerLiteral node) {
		// {integer_literal} [value]:number
		// aqui tem uma gambiarra, de dizer que temos um tipo int
		this.type = new IntegerType(node.line, node.row);
	}

	private static VarInfo getVariable(ClassInfo ci, MethodInfo mi, Symbol vname) {
		// procura nos atributos da classe
        VarInfo vinfo = ci.attributes.get(vname);

        if (vinfo != null) {
            return vinfo;
        }

        // procura nos parametros dos metodo
        vinfo = mi.formalsTable.get(vname);

        if (vinfo != null) {
            return vinfo;
        }

        // procura nas variaveis locais
        // se nao achar ateh aqui, tem que retornar null
        return mi.localsTable.get(vname);
	}
	
	public static MethodInfo getMethod(ClassInfo cinfo, String name)
	{
		MethodInfo mi = null;
		ClassInfo ci = cinfo;
		while(ci != null && mi == null)
		{
			mi = ci.methods.get(Symbol.symbol(name));
			ci = ci.base;
		}	
		
		return mi;
	}
}
